fix: frontend state management bugs and performance#715
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughMemoizes a sidebar item, migrates canvas/sheet views to a per-page document manager with debounced saves and unmount flush, syncs edited page titles to open-tabs/tabs store, adds AbortController-backed permission checks with toast, and changes SWR pausing to require an initial successful load before pausing during editing. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Canvas as CanvasPageView
participant DocManager as DocumentManagerStore
participant SaveSched as SaveScheduler
participant API as API
User->>Canvas: edit content
Canvas->>DocManager: setContent(newContent) [mark isDirty, inc version]
Canvas->>SaveSched: schedule debounced save
SaveSched-->>Canvas: debounce fires
Canvas->>API: saveContent(latestContent, version)
API-->>Canvas: save success
Canvas->>DocManager: clear isDirty if version matches, update lastSaved
User->>Canvas: navigate away / unmount
Canvas->>Canvas: if isDirty -> force save before unmount
sequenceDiagram
actor User
participant Editable as EditableTitle
participant API as API
participant OpenTabs as OpenTabsStore
participant Tabs as TabsStore
User->>Editable: submit new title
Editable->>API: PATCH /pages/:id {title}
API-->>Editable: 200 OK (updated page)
Editable->>OpenTabs: update open tab title for pageId
Editable->>Tabs: updateTabMetaByPageId(pageId, {title: newTitle})
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8c02eca689
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
apps/web/src/components/layout/middle-content/page-views/canvas/CanvasPageView.tsx
Show resolved
Hide resolved
|
Re: Codex review on CanvasPageView cleanup (P1: flush pending edits) Addressed in commit 7c02f1d. The cleanup effect was split into two:
Additionally, the debounced save timeout now resets |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@apps/web/src/components/layout/middle-content/page-views/canvas/CanvasPageView.tsx`:
- Around line 52-73: The debounce currently catches all errors so the
success-path updateDocument call clears isDirty even when save fails; remove the
empty catch here (or rethrow the error) so failures from saveContent(page.id,
newContent) propagate and prevent updating isDirty. Update the setContent
callback (inside the setTimeout) to either await saveContent without a try/catch
or, if you must catch to log, rethrow the caught error; ensure saveContent still
throws on failure so useDocumentManagerStore.getState().updateDocument(page.id,
{ isDirty: false, lastSaved: ... }) only runs when saveContent resolves
successfully.
- Around line 88-92: The mounted effect calls
useDocumentManagerStore.getState().createDocument(page.id, ...) but
createDocument is a no-op if the doc exists, causing stale content when
revisiting; fix by first reading the cached document via
useDocumentManagerStore.getState().getDocument(page.id) (or equivalent getter)
and if it exists compare its text to the fresh page.content and call an update
method (e.g., updateDocument or createDocument with overwrite) when they differ;
additionally add a cleanup in the same useEffect that removes or clears the
document on unmount (e.g.,
useDocumentManagerStore.getState().deleteDocument(page.id) or resetDocument) so
documents don't persist across remounts when out-of-band server changes occur.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
apps/web/src/components/layout/left-sidebar/page-tree/PageTreeItem.tsxapps/web/src/components/layout/middle-content/content-header/EditableTitle.tsxapps/web/src/components/layout/middle-content/page-views/canvas/CanvasPageView.tsxapps/web/src/components/layout/middle-content/page-views/document/DocumentView.tsxapps/web/src/components/layout/middle-content/page-views/sheet/SheetView.tsxapps/web/src/hooks/page-agents/usePageAgents.tsapps/web/src/hooks/useDevices.tsapps/web/src/stores/useDocumentStore.ts
apps/web/src/components/layout/middle-content/page-views/canvas/CanvasPageView.tsx
Show resolved
Hide resolved
apps/web/src/components/layout/middle-content/page-views/canvas/CanvasPageView.tsx
Outdated
Show resolved
Hide resolved
Addressing Review Feedback (d556e65)Thread 1: Codex Bot P1 — Flush pending canvas edits before clearing debounce timerThe codex bot reviewed commit 8c02eca which didn't have the force-save on unmount. Commit 7c02f1d added it, and d556e65 further strengthens it:
Thread 2: CodeRabbit Critical — Debounced save can clear isDirty even when save failsFixed. Thread 3: CodeRabbit Major — Stale content shown when revisiting canvas pageFixed. The initialize effect now checks for an existing cached document and refreshes it from the prop if the content differs and the doc isn't dirty. On unmount, CI Fix: useDevices.test.ts (2 failing tests)Updated tests to match the User Review: P1 — Keep canvas dirty state until latest save confirmedFixed via |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
apps/web/src/hooks/__tests__/useDevices.test.ts (1)
75-75: GuardonSuccesswithtoBeDefinedbefore calling it via non-null assertion.Both lines call
swrConfig.onSuccess!()without first asserting the field exists. IfonSuccessis ever removed fromuseDevices.ts, the tests will throw an unguarded runtime error instead of a clean, descriptive assertion failure.🛡️ Proposed fix (shown for line 75; apply the same pattern at line 101)
+ expect(swrConfig.onSuccess).toBeDefined(); swrConfig.onSuccess!();Also applies to: 101-101
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/hooks/__tests__/useDevices.test.ts` at line 75, Add a guard asserting that swrConfig.onSuccess is defined before invoking it in the test: replace the direct non-null assertion call with an assertion like expect(swrConfig.onSuccess).toBeDefined(); followed by the call to swrConfig.onSuccess!(); Apply this change for both places where swrConfig.onSuccess is invoked in the useDevices.test.ts tests so the test fails with a clear assertion if the handler is removed.apps/web/src/components/layout/middle-content/page-views/canvas/CanvasPageView.tsx (1)
246-251: React.memo comparator is appropriate but note the coupling with unmount cleanup.The comparator correctly limits re-renders to changes in
page.idandpage.content. However, because it allows re-renders onpage.idchange (not just remounts), the empty-deps unmount effect (lines 112-124) must use refs to access the currentpage.id— see related comment above.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/components/layout/middle-content/page-views/canvas/CanvasPageView.tsx` around lines 246 - 251, The memo comparator on CanvasPageView only prevents renders when page.id and page.content are unchanged, but because CanvasPageView's empty-deps unmount effect (the cleanup run on component unmount) currently closes over the original page.id, update that effect to read the current page id from a ref instead of the stale prop: add a pageIdRef (e.g., const pageIdRef = useRef(page.id)), update pageIdRef.current on each render or in a useEffect that depends on page.id, and then reference pageIdRef.current inside the cleanup callback so cleanup uses the latest page id when the component unmounts; keep the React.memo comparator as-is (export default React.memo(CanvasPageView, ...)).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@apps/web/src/components/layout/middle-content/page-views/canvas/CanvasPageView.tsx`:
- Around line 112-124: The cleanup effect captures a stale page.id; update the
component to track the current page id in a ref (e.g., pageIdRef) and keep it in
sync whenever props.page.id changes, then use pageIdRef.current inside the
useEffect cleanup instead of the closed-over page.id; specifically, create
pageIdRef (like saveContentRef), set pageIdRef.current = page.id in a short
effect that runs on page.id changes, and in the existing useEffect cleanup
replace references to page.id with pageIdRef.current when calling
saveContentRef.current(...) and
useDocumentManagerStore.getState().clearDocument(...).
- Around line 23-32: CanvasPageView never registers editing with the global
editing store; add a useEffect inside the CanvasPageView component that calls
useEditingStore.getState().startEditing() when the component enters edit mode
(e.g., when activeTab === 'edit' or when the editor is focused) and returns a
cleanup that calls useEditingStore.getState().stopEditing() (or the appropriate
end method) to unregister on unmount or when leaving edit mode; place this
effect in CanvasPageView near the other refs/hooks (alongside
containerRef/saveTimeoutRef) so SWR revalidation is suppressed while editing.
---
Nitpick comments:
In
`@apps/web/src/components/layout/middle-content/page-views/canvas/CanvasPageView.tsx`:
- Around line 246-251: The memo comparator on CanvasPageView only prevents
renders when page.id and page.content are unchanged, but because
CanvasPageView's empty-deps unmount effect (the cleanup run on component
unmount) currently closes over the original page.id, update that effect to read
the current page id from a ref instead of the stale prop: add a pageIdRef (e.g.,
const pageIdRef = useRef(page.id)), update pageIdRef.current on each render or
in a useEffect that depends on page.id, and then reference pageIdRef.current
inside the cleanup callback so cleanup uses the latest page id when the
component unmounts; keep the React.memo comparator as-is (export default
React.memo(CanvasPageView, ...)).
In `@apps/web/src/hooks/__tests__/useDevices.test.ts`:
- Line 75: Add a guard asserting that swrConfig.onSuccess is defined before
invoking it in the test: replace the direct non-null assertion call with an
assertion like expect(swrConfig.onSuccess).toBeDefined(); followed by the call
to swrConfig.onSuccess!(); Apply this change for both places where
swrConfig.onSuccess is invoked in the useDevices.test.ts tests so the test fails
with a clear assertion if the handler is removed.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/web/src/components/layout/middle-content/page-views/canvas/CanvasPageView.tsxapps/web/src/hooks/__tests__/useDevices.test.ts
apps/web/src/components/layout/middle-content/page-views/canvas/CanvasPageView.tsx
Show resolved
Hide resolved
apps/web/src/components/layout/middle-content/page-views/canvas/CanvasPageView.tsx
Outdated
Show resolved
Hide resolved
Addressing Round 2 Review Feedback (e1e70a3)Thread 4: CodeRabbit — Missing useEditingStore registrationFixed. Added Thread 5: CodeRabbit — Stale page.id in unmount cleanupFixed. Added Note: The parent ( |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/src/components/layout/middle-content/page-views/canvas/CanvasPageView.tsx (1)
140-173:⚠️ Potential issue | 🟡 MinorMissing
AbortController— stale fetch can overwrite freshly-initialized content on rapid remount.
fetchWithAuthon line 155 is not aborted when the component unmounts or the socket listener is torn down. The unmount cleanup callsclearDocument, so a late response arriving after unmount is a safe no-op. However, if the parent remounts the component with the samepage.idbefore the fetch resolves (the init effect will have re-created the document), the stale response will callupdateContentFromServeron the newly-created document, potentially replacing fresher content with an older snapshot.The PR already uses
AbortControllerinDocumentViewandSheetViewfor permission checks; apply the same pattern here.🛠️ Proposed fix
useEffect(() => { if (!socket) return; + let abortController: AbortController | null = null; const handleContentUpdate = async (eventData: PageEventPayload) => { if (eventData.socketId && eventData.socketId === socket.id) return; if (eventData.pageId === page.id) { console.log(`[Canvas] Received content update for page ${page.id}`); try { + abortController = new AbortController(); - const response = await fetchWithAuth(`/api/pages/${page.id}`); + const response = await fetchWithAuth(`/api/pages/${page.id}`, { signal: abortController.signal }); if (response.ok) { const updatedPage = await response.json(); const newContent = typeof updatedPage.content === 'string' ? updatedPage.content : ''; updateContentFromServer(newContent); } } catch (error) { + if (error instanceof DOMException && error.name === 'AbortError') return; console.error('Failed to fetch updated canvas content:', error); } } }; socket.on('page:content-updated', handleContentUpdate); return () => { socket.off('page:content-updated', handleContentUpdate); + abortController?.abort(); }; }, [socket, page.id, updateContentFromServer]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/components/layout/middle-content/page-views/canvas/CanvasPageView.tsx` around lines 140 - 173, The socket handler handleContentUpdate must use an AbortController so stale fetches don't overwrite newly-initialized documents: inside the useEffect create a controller-per-invocation (e.g., create a new AbortController at the top of handleContentUpdate or push it into an array controllers declared in the effect scope), pass controller.signal into fetchWithAuth, and on cleanup for the effect abort all active controllers and remove the socket listener via socket.off('page:content-updated', handleContentUpdate); also make the catch ignore AbortError (or skip calling updateContentFromServer if the fetch was aborted) to avoid applying stale content; reference handleContentUpdate, fetchWithAuth, updateContentFromServer, and the socket.on/off setup when making the change.
🧹 Nitpick comments (2)
apps/web/src/components/layout/middle-content/page-views/canvas/CanvasPageView.tsx (2)
82-93:updateContentFromServershould also bumpsaveVersionRefto invalidate any in-flight debounced save.
clearTimeoutonly cancels a timer that hasn't fired yet. If the debounced callback is already mid-execution (i.e., awaitingsaveContent(...)) when a server update arrives,clearTimeoutis a no-op for it. That in-flight save will still complete, pass thesaveVersionRef.current === versionguard, and overwritelastSavedwith a stale timestamp — even though the authoritative content now came from the server.Bumping the version counter prevents this:
♻️ Proposed fix
const updateContentFromServer = useCallback((newContent: string) => { if (saveTimeoutRef.current) { clearTimeout(saveTimeoutRef.current); saveTimeoutRef.current = null; } + // Invalidate any in-flight debounced save so its version guard fails + saveVersionRef.current++; useDocumentManagerStore.getState().updateDocument(page.id, { content: newContent, isDirty: false, lastSaved: Date.now(), lastUpdateTime: Date.now(), }); }, [page.id]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/components/layout/middle-content/page-views/canvas/CanvasPageView.tsx` around lines 82 - 93, The updateContentFromServer handler currently clears the debounce timer but does not increment the in-flight save version, so an already-running save (awaiting saveContent) can still pass the saveVersionRef.current === version check and overwrite server-updated state; inside updateContentFromServer (function name) after clearing saveTimeoutRef (saveTimeoutRef.current) and before calling updateDocument, increment or bump saveVersionRef.current (e.g., ++saveVersionRef.current or assign a new token) so any in-flight save will fail its version guard (saveVersionRef and the saveContent version check) and be prevented from writing stale lastSaved/lastUpdateTime.
259-264: Custommemocomparator silently suppresses re-renders for any newTreePagefields added in the future.The comparator is correct for the current component surface (
page.idandpage.contentare the onlypagefields consumed). Consider adding a brief comment so future contributors know why only these two fields are compared, to avoid accidentally introducing silent stale-prop bugs if newpagefields (e.g.,page.title,page.permissions) are later referenced in the component.♻️ Suggested comment
export default React.memo( CanvasPageView, + // Only page.id and page.content drive this component's rendering and effects. + // If you add consumption of other page fields, extend this comparator accordingly. (prevProps, nextProps) => prevProps.page.id === nextProps.page.id && prevProps.page.content === nextProps.page.content );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/components/layout/middle-content/page-views/canvas/CanvasPageView.tsx` around lines 259 - 264, The custom React.memo comparator on CanvasPageView currently only compares prevProps.page.id and prevProps.page.content which will silently prevent re-renders if new TreePage fields are later used; update the memo call by adding a concise comment above the comparator explaining that the comparator intentionally limits checks to page.id and page.content (and that any time a new page field like page.title or page.permissions is consumed by CanvasPageView the comparator must be updated to include it or be removed), and ensure the comment references CanvasPageView, prevProps/nextProps and the page.id/page.content checks so future contributors understand why only those fields are compared.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In
`@apps/web/src/components/layout/middle-content/page-views/canvas/CanvasPageView.tsx`:
- Around line 140-173: The socket handler handleContentUpdate must use an
AbortController so stale fetches don't overwrite newly-initialized documents:
inside the useEffect create a controller-per-invocation (e.g., create a new
AbortController at the top of handleContentUpdate or push it into an array
controllers declared in the effect scope), pass controller.signal into
fetchWithAuth, and on cleanup for the effect abort all active controllers and
remove the socket listener via socket.off('page:content-updated',
handleContentUpdate); also make the catch ignore AbortError (or skip calling
updateContentFromServer if the fetch was aborted) to avoid applying stale
content; reference handleContentUpdate, fetchWithAuth, updateContentFromServer,
and the socket.on/off setup when making the change.
---
Nitpick comments:
In
`@apps/web/src/components/layout/middle-content/page-views/canvas/CanvasPageView.tsx`:
- Around line 82-93: The updateContentFromServer handler currently clears the
debounce timer but does not increment the in-flight save version, so an
already-running save (awaiting saveContent) can still pass the
saveVersionRef.current === version check and overwrite server-updated state;
inside updateContentFromServer (function name) after clearing saveTimeoutRef
(saveTimeoutRef.current) and before calling updateDocument, increment or bump
saveVersionRef.current (e.g., ++saveVersionRef.current or assign a new token) so
any in-flight save will fail its version guard (saveVersionRef and the
saveContent version check) and be prevented from writing stale
lastSaved/lastUpdateTime.
- Around line 259-264: The custom React.memo comparator on CanvasPageView
currently only compares prevProps.page.id and prevProps.page.content which will
silently prevent re-renders if new TreePage fields are later used; update the
memo call by adding a concise comment above the comparator explaining that the
comparator intentionally limits checks to page.id and page.content (and that any
time a new page field like page.title or page.permissions is consumed by
CanvasPageView the comparator must be updated to include it or be removed), and
ensure the comment references CanvasPageView, prevProps/nextProps and the
page.id/page.content checks so future contributors understand why only those
fields are compared.
- Migrate CanvasPageView from useDocumentStore to useDocumentManagerStore to fix shared saveTimeoutId data loss bug with parallel documents - Add hasLoadedRef guard to useDevices and usePageAgents SWR isPaused to prevent blocking initial data fetch when editing is active - Move SheetView socket listener deps to refs to stop re-subscribing on every keystroke (documentState.content was in useEffect deps) - Wrap PageTreeItem with React.memo to prevent re-rendering every tree node on any parent state change - Update EditableTitle to sync renamed page title to both tab stores (useOpenTabsStore and useTabsStore) - Add AbortController to permission check fetches in DocumentView and SheetView to prevent stale responses overwriting isReadOnly state Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address two issues from code review: - isDirty flag now reset to false after debounced save succeeds - Dirty content is force-saved on component unmount, matching the pattern used in DocumentView and SheetView Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rded isDirty, stale content - saveContent now rethrows errors so debounced save won't clear isDirty on failure - Add saveVersionRef counter so only the latest save can mark document clean, preventing a race where an older in-flight save clears isDirty while newer edits are unsaved - Initialize effect now refreshes cached doc from props when content differs and doc isn't dirty, fixing stale content on page revisit - Unmount cleanup now calls clearDocument after force-save to prevent stale cache across remounts - Update useDevices tests to match hasLoadedRef behavior: isPaused only blocks revalidation after initial fetch completes via onSuccess Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…leanup - Register editing state with useEditingStore when content is dirty, preventing SWR revalidation from disrupting canvas edits (per CLAUDE.md UI Refresh Protection guidelines) - Add pageIdRef to avoid stale page.id capture in empty-deps unmount cleanup, making the pattern resilient even without key-based remounting - Clean up useEditingStore session on unmount alongside document cache - Remove unused eslint-disable directive Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
e1e70a3 to
e6147c6
Compare
…t fixes Reviews all 9 changed files for correctness, performance, security (OWASP top 10), and adherence to project patterns. Score: 88/100 — approved with follow-up items for CanvasPageView test coverage. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… tests
- fix(canvas): only clearDocument after successful unmount save; keep dirty
document in store on failure so content can be recovered on remount
- fix(canvas): guard updateContentFromServer against overwriting local dirty
edits or in-flight debounced saves (conflict detection)
- fix(page-agents): reset hasLoadedRef when SWR key changes so new key's
initial fetch isn't incorrectly paused
- fix(editable-title): use path.split('/').pop() instead of fragile endsWith
for tab-to-page matching
- test: add CanvasPageView save lifecycle tests (10 tests covering debounce,
version guard, error propagation, unmount force-save, conflict detection)
- test: add usePageAgents hasLoadedRef tests (4 tests including key-change reset)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…SWR key reset - [HIGH] CanvasPageView unmount: only clearDocument after save succeeds; on failure, keep document state so user can retry (prevents data loss) - [Medium] updateContentFromServer: skip server update when a pending local save exists (saveTimeoutRef.current is set) to avoid overwriting in-progress edits during concurrent socket updates - [Medium] usePageAgents: reset hasLoadedRef when SWR key changes so the initial fetch for a new key is not incorrectly paused - [Low] EditableTitle: batch tab-title updates via new updateTabMetaByPageId store action (single set() call); uses path segment matching instead of fragile endsWith - [Medium] Add canvas save lifecycle tests (debounce, version guard, error propagation, unmount force-save, server update guard) - [Low] Add usePageAgents hasLoadedRef tests including key-change reset Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… updates Without this, the stale timeout ID kept saveTimeoutRef truthy after the debounce fired, causing updateContentFromServer to reject incoming server updates even when no debounce was pending. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
apps/web/src/hooks/page-agents/__tests__/usePageAgents.test.ts (1)
53-55: Optional: extract SWR config lookup helper to reduce repetition.The same mocked call indexing/cast pattern appears multiple times; a small helper would make the tests easier to maintain.
♻️ Proposed refactor
+type SwrConfigUnderTest = { + isPaused?: () => boolean; + onSuccess?: () => void; +}; + +const getSwrConfig = (): SwrConfigUnderTest => { + const swrCall = vi.mocked(useSWR).mock.calls[0]; + return (swrCall?.[2] ?? {}) as SwrConfigUnderTest; +}; - const swrCall = vi.mocked(useSWR).mock.calls[0]; - const swrConfig = swrCall[2] as { isPaused?: () => boolean; onSuccess?: () => void }; + const swrConfig = getSwrConfig(); - const swrCall = vi.mocked(useSWR).mock.calls[0]; - const swrConfig = swrCall[2] as { isPaused?: () => boolean; onSuccess?: () => void }; + const swrConfig = getSwrConfig(); - const swrCall = vi.mocked(useSWR).mock.calls[0]; - const swrConfig = swrCall[2] as { isPaused?: () => boolean; onSuccess?: () => void }; + const swrConfig = getSwrConfig(); - const firstCall = vi.mocked(useSWR).mock.calls[0]; - const firstConfig = firstCall[2] as { isPaused?: () => boolean; onSuccess?: () => void }; + const firstConfig = getSwrConfig(); - const secondCall = vi.mocked(useSWR).mock.calls[0]; - const secondConfig = secondCall[2] as { isPaused?: () => boolean; onSuccess?: () => void }; + const secondConfig = getSwrConfig();Also applies to: 66-68, 82-84, 106-107, 119-120
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/hooks/page-agents/__tests__/usePageAgents.test.ts` around lines 53 - 55, The tests repeatedly extract the SWR mock call and its config using the same pattern (vi.mocked(useSWR).mock.calls[...] and casting to the config shape); create a small helper function (e.g., getMockSWRConfig or extractSWRConfig) used by tests in usePageAgents.test.ts to accept the call index (or default to 0) and return the typed config object so you can replace repeated lines that compute swrCall and swrConfig (references: useSWR, swrCall, swrConfig) with a single call to the helper to reduce duplication and improve maintainability.apps/web/src/components/layout/middle-content/page-views/canvas/__tests__/CanvasPageView.save.test.ts (1)
79-281: These tests don’t executeCanvasPageViewsave logic.Most assertions directly mutate store state and call mocks, so regressions in debounce/unmount effects inside the component can still pass unnoticed.
Refactor direction
- Keep one or two pure store tests.
- Move lifecycle assertions to component-level tests (render
CanvasPageView, triggerMonacoEditoronChange, advance fake timers, unmount, then assert store + patch interactions).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/components/layout/middle-content/page-views/canvas/__tests__/CanvasPageView.save.test.ts` around lines 79 - 281, Tests are mutating useDocumentManagerStore and calling mockPatch directly so they never exercise CanvasPageView's debounce/unmount save effects; refactor by keeping 1–2 pure store unit tests but move lifecycle assertions into component-level tests that render CanvasPageView, simulate user edits by invoking the MonacoEditor onChange handler (or dispatching events that trigger CanvasPageView's update flow), advance fake timers to flush the debounce, unmount the component to trigger the force-save effect, and then assert store state and mockPatch calls to verify real save/version-guard/error behaviors.apps/web/src/components/layout/middle-content/page-views/canvas/__tests__/canvas-save-lifecycle.test.ts (1)
1-165: Consolidate overlapping save-lifecycle suites.This file overlaps heavily with
CanvasPageView.save.test.ts; keeping both will likely drift and increase maintenance overhead.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/components/layout/middle-content/page-views/canvas/__tests__/canvas-save-lifecycle.test.ts` around lines 1 - 165, The test file defines a save-lifecycle suite that overlaps with CanvasPageView.save.test.ts; remove duplication by consolidating behaviors into a single canonical test file: either delete this file (apps/web/src/components/layout/middle-content/page-views/canvas/__tests__/canvas-save-lifecycle.test.ts) or merge any unique specs here into CanvasPageView.save.test.ts, ensuring you keep the authoritative tests for debounce/version-guard/error/unmount scenarios (tests referencing useDocumentManagerStore, createDocument, updateDocument, clearDocument) and remove the redundant suite, update any test imports or CI references, and run the test suite to confirm no regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@apps/web/src/components/layout/middle-content/page-views/canvas/__tests__/CanvasPageView.save.test.ts`:
- Around line 52-53: The test file uses the React namespace in the ErrorBoundary
prop type (ErrorBoundary: ({ children }: { children: React.ReactNode }) =>
children) but never imports React; add a proper React type import and update the
type usage accordingly — e.g., import type { ReactNode } from 'react' and change
the prop annotation to ({ children }: { children: ReactNode }) or alternatively
import React and keep React.ReactNode; update the ErrorBoundary declaration to
use the imported type.
In
`@apps/web/src/components/layout/middle-content/page-views/canvas/CanvasPageView.tsx`:
- Around line 134-137: The post-save .then() unconditionally calls
useDocumentManagerStore.getState().clearDocument(id), which can clear a freshly
remounted document if the component remounts before the save resolves; change
the callback to guard against remount races by verifying the saved id still
represents the same live document before clearing—e.g., capture a local mount
token or check a current active document id/version from useDocumentManagerStore
(or an isMounted flag) and only call clearDocument(id) when the token/active id
matches the original id; target saveContentRef.current(...)'s .then() handler
and replace the unconditional clearDocument call with this guarded check.
In
`@apps/web/src/components/layout/middle-content/page-views/sheet/SheetView.tsx`:
- Around line 1550-1553: The fetchWithAuth call in SheetView.tsx constructs a
query string with user.id unencoded which can break URLs if the ID contains
reserved characters; update the call that builds
`/api/pages/${page.id}/permissions/check?userId=${user.id}` to encode the user
id (e.g., use encodeURIComponent(user.id)) before interpolation so the query
parameter is safely encoded while keeping the same abortController.signal and
existing fetchWithAuth usage.
---
Nitpick comments:
In
`@apps/web/src/components/layout/middle-content/page-views/canvas/__tests__/canvas-save-lifecycle.test.ts`:
- Around line 1-165: The test file defines a save-lifecycle suite that overlaps
with CanvasPageView.save.test.ts; remove duplication by consolidating behaviors
into a single canonical test file: either delete this file
(apps/web/src/components/layout/middle-content/page-views/canvas/__tests__/canvas-save-lifecycle.test.ts)
or merge any unique specs here into CanvasPageView.save.test.ts, ensuring you
keep the authoritative tests for debounce/version-guard/error/unmount scenarios
(tests referencing useDocumentManagerStore, createDocument, updateDocument,
clearDocument) and remove the redundant suite, update any test imports or CI
references, and run the test suite to confirm no regressions.
In
`@apps/web/src/components/layout/middle-content/page-views/canvas/__tests__/CanvasPageView.save.test.ts`:
- Around line 79-281: Tests are mutating useDocumentManagerStore and calling
mockPatch directly so they never exercise CanvasPageView's debounce/unmount save
effects; refactor by keeping 1–2 pure store unit tests but move lifecycle
assertions into component-level tests that render CanvasPageView, simulate user
edits by invoking the MonacoEditor onChange handler (or dispatching events that
trigger CanvasPageView's update flow), advance fake timers to flush the
debounce, unmount the component to trigger the force-save effect, and then
assert store state and mockPatch calls to verify real save/version-guard/error
behaviors.
In `@apps/web/src/hooks/page-agents/__tests__/usePageAgents.test.ts`:
- Around line 53-55: The tests repeatedly extract the SWR mock call and its
config using the same pattern (vi.mocked(useSWR).mock.calls[...] and casting to
the config shape); create a small helper function (e.g., getMockSWRConfig or
extractSWRConfig) used by tests in usePageAgents.test.ts to accept the call
index (or default to 0) and return the typed config object so you can replace
repeated lines that compute swrCall and swrConfig (references: useSWR, swrCall,
swrConfig) with a single call to the helper to reduce duplication and improve
maintainability.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
apps/web/src/components/layout/left-sidebar/page-tree/PageTreeItem.tsxapps/web/src/components/layout/middle-content/content-header/EditableTitle.tsxapps/web/src/components/layout/middle-content/page-views/canvas/CanvasPageView.tsxapps/web/src/components/layout/middle-content/page-views/canvas/__tests__/CanvasPageView.save.test.tsapps/web/src/components/layout/middle-content/page-views/canvas/__tests__/canvas-save-lifecycle.test.tsapps/web/src/components/layout/middle-content/page-views/document/DocumentView.tsxapps/web/src/components/layout/middle-content/page-views/sheet/SheetView.tsxapps/web/src/hooks/__tests__/useDevices.test.tsapps/web/src/hooks/page-agents/__tests__/usePageAgents.test.tsapps/web/src/hooks/page-agents/usePageAgents.tsapps/web/src/hooks/useDevices.tsapps/web/src/stores/useDocumentStore.tsapps/web/src/stores/useTabsStore.ts
✅ Files skipped from review due to trivial changes (1)
- apps/web/src/hooks/page-agents/usePageAgents.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/web/src/stores/useDocumentStore.ts
- apps/web/src/components/layout/middle-content/page-views/document/DocumentView.tsx
...src/components/layout/middle-content/page-views/canvas/__tests__/CanvasPageView.save.test.ts
Outdated
Show resolved
Hide resolved
apps/web/src/components/layout/middle-content/page-views/canvas/CanvasPageView.tsx
Outdated
Show resolved
Hide resolved
apps/web/src/components/layout/middle-content/page-views/sheet/SheetView.tsx
Show resolved
Hide resolved
…ding - Guard post-unmount clearDocument with lastUpdateTime snapshot so a remount race can't clear the new component's document state - Add ReactNode type import in CanvasPageView.save.test.ts (was using unimported React.ReactNode) - Encode user.id with encodeURIComponent in permission-check URLs (SheetView + DocumentView) to handle reserved characters safely Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/src/components/layout/middle-content/page-views/document/DocumentView.tsx (1)
131-152:⚠️ Potential issue | 🟡 MinorFail closed while permission state is unresolved.
isReadOnlycan temporarily retain the previous page’s value until the new request resolves, which can briefly enable edits on a page that may be read-only. SetisReadOnly(true)before the request and keep it true on non-OK/error paths.🔧 Suggested adjustment
useEffect(() => { const abortController = new AbortController(); const checkPermissions = async () => { if (!user?.id) return; + setIsReadOnly(true); try { const response = await fetchWithAuth( `/api/pages/${pageId}/permissions/check?userId=${encodeURIComponent(user.id)}`, { signal: abortController.signal } ); if (response.ok) { const permissions = await response.json(); setIsReadOnly(!permissions.canEdit); if (!permissions.canEdit) { toast.info("You don't have permission to edit this document", { duration: 4000, position: 'bottom-right' }); } + } else { + setIsReadOnly(true); } } catch (error) { if ((error as Error).name === 'AbortError') return; + setIsReadOnly(true); console.error('Failed to check permissions:', error); } };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/components/layout/middle-content/page-views/document/DocumentView.tsx` around lines 131 - 152, In checkPermissions, fail closed by setting isReadOnly(true) immediately before making the fetch and ensure any non-OK or error paths (except AbortError) leave isReadOnly true; then only call setIsReadOnly(!permissions.canEdit) after a successful response.ok and JSON parse. Update the fetchWithAuth handling in checkPermissions to set isReadOnly(true) prior to the request, set isReadOnly(true) when response.ok is false, and in the catch block (except when error.name === 'AbortError') also set isReadOnly(true) before logging the error so the previous page’s writable state cannot leak.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@apps/web/src/components/layout/middle-content/page-views/canvas/CanvasPageView.tsx`:
- Around line 25-27: The initial render falls back to an empty string causing a
flash of blank content; update the content fallback in the component to use the
page's server-provided content instead of '' — i.e., where you derive content
from useDocumentManagerStore (variable documentState and constant content),
change the fallback from '' to page.content so first paint shows initial
content; also sweep the other occurrences mentioned (the similar logic around
lines 98-111) and apply the same change to any other uses that default to ''
when documentState is not yet populated.
- Around line 66-76: saveContent calls from CanvasPageView currently omit
expectedRevision, allowing stale PATCHes to overwrite newer edits; either (A)
add revision tracking to the document store (e.g., store currentRevision on the
page state in useDocumentManagerStore and keep it updated when server responses
arrive) and change saveContent calls in CanvasPageView (and the unmount cleanup
path where saveTimeoutRef/ saveVersionRef are used) to include expectedRevision
when PATCHing, or (B) enforce single in-flight save by wiring an AbortController
per-page: store the controller on save start, pass its signal into saveContent,
abort any prior controller before starting a new save, and also abort pending
saves on unmount so stale requests cannot complete. Ensure references to
saveContent, saveTimeoutRef, saveVersionRef, and
useDocumentManagerStore.updateDocument are updated accordingly.
---
Outside diff comments:
In
`@apps/web/src/components/layout/middle-content/page-views/document/DocumentView.tsx`:
- Around line 131-152: In checkPermissions, fail closed by setting
isReadOnly(true) immediately before making the fetch and ensure any non-OK or
error paths (except AbortError) leave isReadOnly true; then only call
setIsReadOnly(!permissions.canEdit) after a successful response.ok and JSON
parse. Update the fetchWithAuth handling in checkPermissions to set
isReadOnly(true) prior to the request, set isReadOnly(true) when response.ok is
false, and in the catch block (except when error.name === 'AbortError') also set
isReadOnly(true) before logging the error so the previous page’s writable state
cannot leak.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
apps/web/src/components/layout/middle-content/page-views/canvas/CanvasPageView.tsxapps/web/src/components/layout/middle-content/page-views/canvas/__tests__/CanvasPageView.save.test.tsapps/web/src/components/layout/middle-content/page-views/document/DocumentView.tsxapps/web/src/components/layout/middle-content/page-views/sheet/SheetView.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web/src/components/layout/middle-content/page-views/canvas/tests/CanvasPageView.save.test.ts
apps/web/src/components/layout/middle-content/page-views/canvas/CanvasPageView.tsx
Show resolved
Hide resolved
apps/web/src/components/layout/middle-content/page-views/canvas/CanvasPageView.tsx
Show resolved
Hide resolved
…tedRevision with saves - Use page.content as content fallback instead of empty string to prevent blank flash before document store hydrates - Send expectedRevision with PATCH requests for optimistic locking - Track and update revision from server responses (saves and socket updates) - Seed revision from page props on document initialization - Include page.revision in React.memo comparator Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
useDocumentStore(sharedsaveTimeoutId) touseDocumentManagerStore(per-documentMap<string, DocumentState>)hasLoadedRefguard touseDevicesandusePageAgentssoisPausednever blocks the first fetchdocumentState.content/isDirtyreads into refs so the socketuseEffectdoesn't re-subscribe on every keystrokeEditableTitlenow syncs renamed page title to bothuseOpenTabsStoreanduseTabsStoreAbortControllerto permission checks inDocumentViewandSheetViewso stale responses don't overwriteisReadOnlyisDirtyafter successful debounced save; force-save dirty content on unmountsaveContentnow rethrows so failed saves leaveisDirty=trueTest plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Improvements
Tests